package com.nulldev.util.web.flexibleKit.scfk;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import java.util.Map;

import com.nulldev.util.data.Variables;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.extensions.copyable.Copyable;
import com.nulldev.util.io.extensions.copyable.nio.NIOCopyable;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.util.HttpStreamUtil;
import com.nulldev.util.web.flexibleKit.scfk.io.streams.Stream;
import com.nulldev.util.web.flexibleKit.scfk.io.streams.StreamManager;
import com.nulldev.util.web.flexibleKit.scfk.protocols.PreferredProtocol;

public class SCFKResponse implements AutoCloseable, Copyable, NIOCopyable {

	private static final Logger log = LoggerFactory.getLogger();
	private static final boolean SCFK_IO_USE_AUTO_COPY = true;

	private final PreferredProtocol pp;
	private final int httpRspCode;
	private final Map<String, List<String>> headers;
	private final SCFKRequest r;
	private final Stream s;
	private final InputStream io_stream;

	public SCFKResponse(final PreferredProtocol pp, final int httpRspCode, final Map<String, List<String>> headers, final Stream s, final InputStream io_stream,
			final SCFKRequest r) {
		this.pp = pp;
		this.httpRspCode = httpRspCode;
		this.headers = headers;
		this.s = s;
		this.io_stream = io_stream;
		this.r = r;
	}

	public PreferredProtocol getProtocol() {
		return this.pp;
	}

	public int getResponseCode() {
		return this.httpRspCode;
	}

	public boolean isSuccessful() {
		return this.httpRspCode >= 200 && this.httpRspCode <= 299;
	}

	public Map<String, List<String>> getHeaders() {
		return this.headers;
	}

	private InputStream fioStream;

	private InputStream autoStream() throws IOException {
		if (this.fioStream != null) {
			return this.fioStream;
		} else {
			final boolean chunking = this.headers.containsKey("Transfer-Encoding") && this.headers.get("Transfer-Encoding").get(0).equalsIgnoreCase("chunked");
			final boolean isCompressed = this.headers.containsKey("Content-Encoding");
			log.debug("chunking: " + chunking + ", isEncoded: " + isCompressed);
			if (chunking && isCompressed) {
				return this.fioStream = HttpStreamUtil.decode(this.io_stream, headers.get("Content-Encoding").get(0));
			} else if (isCompressed) {
				log.dwarn("[SCFK::autoStream] Using isCompressed logic on non-chunked stream, this might break!");
				return this.fioStream = HttpStreamUtil.decode(this.io_stream, headers.get("Content-Encoding").get(0));
			} else {
				return this.fioStream = this.io_stream;
			}
		}
	}

	public InputStream asStream() throws IOException {
		return this.autoStream();
	}

	public Reader asReader() throws IOException {
		return new InputStreamReader(this.autoStream());
	}

	public String asString() throws IOException {
		return new String(IOUtils.toByteArray(this.autoStream()));
	}

	public byte[] asByteArray() throws IOException {
		return IOUtils.toByteArray(this.autoStream());
	}

	public boolean willKeepAlive() {
		return this.s.shouldKeepAlive();
	}

	@Override
	public void close() throws IOException {
		this.s.free().close();
		StreamManager.removeFromCache(r.getURL().getHost());
	}

	@Override
	public void writeTo(final OutputStream os) throws IOException {
		Variables.requireNonNull(os, "os");
		final long cl = (this.headers.containsKey("Content-Length") ? Long.parseLong(this.headers.get("Content-Length").get(0)) : -1);
		final InputStream as = this.autoStream();
		if (cl > 0) {
			if (SCFK_IO_USE_AUTO_COPY) {
				IOUtils.autoCopy(as, os, cl);
			} else {
				IOUtils.copy(as, os, cl);
			}
		} else if (cl != 0) {
			if (SCFK_IO_USE_AUTO_COPY) {
				IOUtils.autoCopy(as, os);
			} else {
				IOUtils.copy(as, os);
			}
		} else {
			/* TODO: We should probably have an exception here... */
		}
		this.adaptiveClose();
	}

	private static void __validate_buffer_size(final int size) {
		if (size <= 0)
			throw new IllegalArgumentException("Invalid preferredBufferSize!");
	}

	public void writeTo(final OutputStream os, final int preferredBufferSize) throws IOException {
		Variables.requireNonNull(os, "os");
		__validate_buffer_size(preferredBufferSize);
		final long cl = (this.headers.containsKey("Content-Length") ? Long.parseLong(this.headers.get("Content-Length").get(0)) : -1);
		final InputStream as = this.autoStream();
		if (cl > 0) {
			IOUtils.copyLarge(as, os, new byte[preferredBufferSize], cl);
		} else if (cl != 0) {
			IOUtils.copyLarge(as, os, new byte[preferredBufferSize]);
		} else {
			/* TODO: We should probably have an exception here... */
		}
	}

	public long getContentLength() {
		return (this.headers.containsKey("Content-Length") ? Long.parseLong(this.headers.get("Content-Length").get(0)) : -1);
	}

	@Override
	public void writeTo(final WritableByteChannel os) throws IOException {
		Variables.requireNonNull(os, "os");
		final long cl = (this.headers.containsKey("Content-Length") ? Long.parseLong(this.headers.get("Content-Length").get(0)) : -1);
		final InputStream as = this.autoStream();
		final ReadableByteChannel rdb;
		if (cl > 0) {
			rdb = IOUtils.copyMakeChannelFixed(as, cl);
			IOUtils.fastCopy(rdb, os);
		} else if (cl != 0) {
			rdb = IOUtils.copyMakeChannel(as);
			IOUtils.fastCopy(rdb, os);
		} else
			rdb = null;
		this.adaptiveClose();
		IOUtils.closeQuietly(rdb);
	}

	public boolean isKeepAlive() {
		return this.s.shouldKeepAlive();
	}

	public void adaptiveClose() throws IOException {
		this.s.free().closeIfNeeded();
	}
}
